###########################################
# FP2021/2022 @ IST                       #
# Projeto 2 - O Prado                     #
# Alberto Abad                            #
#                                         # 
# Proposta solucao (sem comentarios       #
# nem documentacao)                       #
###########################################


def cria_posicao(x, y):
    if type(x) == int  and type(y) == int and x >= 0 and y >= 0:
        return {'x':x, 'y':y}

    raise ValueError('cria_posicao: argumentos invalidos')

def cria_copia_posicao(p):
    return {'x':p['x'], 'y':p['y']}

def obter_pos_x(p):
    return p['x']

def obter_pos_y(p):
    return p['y']

def eh_posicao(p):
    return isinstance(p, dict) and len(p) == 2 and 'x' in p and 'y' in p \
           and type(p['x']) == int  and type(p['y']) == int and p['x'] >= 0 and p['y'] >= 0

def posicoes_iguais(p1, p2):
    return eh_posicao(p1) and eh_posicao(p2) and p1['x'] == p2['x'] and p1['y'] == p2['y']

def posicao_para_str(p):
    return '({}, {})'.format(obter_pos_x(p), obter_pos_y(p))

def obter_posicoes_adjacentes(p):
    x, y = obter_pos_x(p), obter_pos_y(p)
    return tuple (cria_posicao(x + dx, y + dy) for dx, dy in ((0,-1), (1,0), (0,1), (-1,0)) if x+dx >=0 and y+dy >=0)

def ordenar_posicoes(t):
    return tuple(sorted(t, key=lambda p: (obter_pos_y(p), obter_pos_x(p))))


def cria_animal(s, r, a):
    if isinstance(s, str) and len(s) > 0 and type(r) == int and r > 0 and type(a) == int and a >= 0:
        return s, r, a, {'fome':0, 'idade':0}

    raise ValueError('cria_animal: argumentos invalidos')

def cria_copia_animal(a):
    return a[0], a[1], a[2], {'fome':a[3]['fome'], 'idade':a[3]['idade']}

def obter_especie(a):
    return a[0]

def obter_freq_reproducao(a):
    return a[1]


def obter_freq_alimentacao(a):
    return a[2]

def obter_fome(a):
    return a[3]['fome'] if eh_predador(a) else 0

def obter_idade(a):
    return a[3]['idade']

def aumenta_idade(a):
    a[3]['idade'] += 1
    return a

def reset_idade(a):
    a[3]['idade'] = 0
    return a


def aumenta_fome(a):
    if eh_predador(a):
        a[3]['fome'] += 1
    return a

def reset_fome(a):
    a[3]['fome'] = 0
    return a


def eh_animal(animal):
    if isinstance(animal, tuple) and len(animal) == 4:
        s, r, a, d = animal
        return isinstance(s, str) and len(s) > 0 and type(r) == int and r > 0 and type(a) == int and a >= 0 and \
               isinstance(d, dict) and len(d) == 2 and 'idade' in d and 'fome' in d \
               and type(d['idade']) == int and type(d['fome']) == int and d['idade'] >= 0 and d['fome'] >= 0
    return False


def eh_predador(a):
    return eh_animal(a) and obter_freq_alimentacao(a) > 0


def eh_presa(a):
    return eh_animal(a) and obter_freq_alimentacao(a) == 0

def animais_iguais(a1, a2):
    return eh_animal(a1) and eh_animal(a2) and obter_especie(a1) == obter_especie(a2) and \
           obter_freq_alimentacao(a1) == obter_freq_alimentacao(a2) and \
           obter_freq_reproducao(a1) == obter_freq_reproducao(a2) and \
           obter_fome(a1) == obter_fome(a2) and \
           obter_idade(a1) == obter_idade(a2)

def animal_para_char(a):
    return obter_especie(a)[0].lower() if eh_presa(a) else obter_especie(a)[0].upper()

def animal_para_str(a):
    return '{} [{}/{}]'.format(obter_especie(a), obter_idade(a), obter_freq_reproducao(a)) if eh_presa(a) else \
        '{} [{}/{};{}/{}]'.format(obter_especie(a), obter_idade(a), obter_freq_reproducao(a), obter_fome(a), obter_freq_alimentacao(a))


def eh_animal_fertil(a):
    return obter_idade(a) >= obter_freq_reproducao(a)

def eh_animal_faminto(a):
    return (not eh_presa(a)) and obter_fome(a) >= obter_freq_alimentacao(a)

def reproduz_animal(a):
    reset_idade(a)
    return cria_animal(obter_especie(a), obter_freq_reproducao(a), obter_freq_alimentacao(a))

def cria_prado(d, r, a, p):
    if eh_posicao(d) and obter_pos_x(d) >=2 and obter_pos_y(d) >=2:
        max_x, max_y = obter_pos_x(d)+1, obter_pos_y(d)+1
        if isinstance(r, tuple) and all(eh_posicao(i) and 0 < obter_pos_x(i) < max_x-1 and 0 < obter_pos_y(i) < max_y -1 for i in r):
            if isinstance(a, tuple) and isinstance(p, tuple) and len(a) == len(p) and len(a) > 0 and \
                    all(eh_animal(i) for i in a) and all(eh_posicao(i) for i in p):
                prado = {'dim': (max_x,  max_y), 'map':{}}
                for i in r:
                    prado['map'][posicao_para_str(i)] = '@'
                for i, j in zip(p, a):
                    prado['map'][posicao_para_str(i)] = j
                return prado

    raise ValueError('cria_prado: argumentos invalidos')


def cria_copia_prado(m):
    return {'dim': (obter_tamanho_x(m), obter_tamanho_y(m)),
            'map': dict((k, cria_copia_animal(v) if eh_animal(v) else '@') for k, v in m['map'].items())}

def obter_tamanho_x(m):
    return m['dim'][0]

def obter_tamanho_y(m):
    return m['dim'][1]

def obter_numero_predadores(m):
    return len([1 for e in m['map'].values() if eh_predador(e)])

def obter_numero_presas(m):
    return len([1 for e in m['map'].values() if eh_presa(e)])

def obter_posicao_animais(m):
    def str_to_posicao(s):
        return cria_posicao(*eval(s))

    return ordenar_posicoes(tuple(str_to_posicao(pos) for pos, val in m['map'].items() if eh_animal(val)))

def obter_animal(m, p):
    return m['map'][posicao_para_str(p)]

def eliminar_animal(m, p):
    del m['map'][posicao_para_str(p)]
    return m

def mover_animal(m, p1, p2):
    m['map'][posicao_para_str(p2)] = m['map'][posicao_para_str(p1)]
    del m['map'][posicao_para_str(p1)]
    return m

def inserir_animal(m, a, p):
    m['map'][posicao_para_str(p)] = a
    return m

def eh_posicao_animal(m, p):
    return posicao_para_str(p) in m['map'] and eh_animal(m['map'][posicao_para_str(p)])

def eh_posicao_obstaculo(m, p):
    return (posicao_para_str(p) in m['map'] and not eh_animal(m['map'][posicao_para_str(p)])) or \
           obter_pos_x(p) == 0 or obter_pos_x(p) == obter_tamanho_x(m) - 1 or \
           obter_pos_y(p) == 0 or obter_pos_y(p) == obter_tamanho_y(m) - 1

def eh_posicao_livre(m, p):
    return posicao_para_str(p) not in m['map']  and \
           0 < obter_pos_x(p) < obter_tamanho_x(m) - 1 and \
           0 < obter_pos_y(p) < obter_tamanho_y(m) - 1

def eh_prado(m):
    return isinstance(m, dict) and len(m) == 2 and 'dim' in m and 'map' in m and \
           isinstance(m['dim'], tuple) and len(m['dim']) == 2 and \
           type(m['dim'][0]) == int and m['dim'][0] >= 3 and \
           type(m['dim'][1]) == int and m['dim'][1] >= 3 and \
           isinstance(m['map'], dict) and all(eh_animal(m['map'][v]) or m['map'][v] == '@' for v in m['map'])


def prados_iguais(m1, m2):
    return eh_prado(m1) and eh_prado(m2) and m1['dim'] == m2['dim'] and len(m1['map']) == len(m2['map']) \
           and all(k in m2['map'] for k in m1['map']) and \
           all(eh_animal(m2['map'][k]) if eh_animal(m1['map'][k]) else not eh_animal(m2['map'][k]) for k in m1['map']) \
           and all(animais_iguais(m1['map'][k], m2['map'][k]) for k in m1['map'] if eh_animal(m1['map'][k]))


def prado_para_str(m):
    mapa_str = '+' + '-'*(obter_tamanho_x(m) - 2) + '+\n'
    for y in range(1, obter_tamanho_y(m)-1):
        mapa_str += '|'
        for x in range(1, obter_tamanho_x(m)-1):
            p = posicao_para_str(cria_posicao(x, y))
            if p in m['map']:
                mapa_str += animal_para_char(m['map'][p]) if eh_animal(m['map'][p]) else '@'
            else:
                mapa_str += '.'
        mapa_str += '|\n'

    mapa_str += '+' + '-' * (obter_tamanho_x(m) - 2) + '+'
    return mapa_str

def obter_valor_numerico(m, p):
    return obter_tamanho_x(m)*obter_pos_y(p) + obter_pos_x(p)

def obter_movimento(m, p):
    if eh_presa(obter_animal(m, p)):
        candidates = [x for x in obter_posicoes_adjacentes(p) if eh_posicao_livre(m, x)]
    else:
        candidates = [x for x in obter_posicoes_adjacentes(p) if eh_posicao_animal(m, x) and eh_presa(obter_animal(m, x))]
        if len(candidates) == 0:
            candidates = [x for x in obter_posicoes_adjacentes(p) if eh_posicao_livre(m, x)]
    return p if not len(candidates) else candidates[obter_valor_numerico(m,p)%len(candidates)]


def geracao(prado):

    positions = obter_posicao_animais(prado)
    already_moved = ()

    while len(positions):
        current, positions = positions[0], positions[1:]
        if posicao_para_str(current) not in already_moved:
            animal = obter_animal(prado, current)

            aumenta_idade(animal)
            aumenta_fome(animal)

            new_pos = obter_movimento(prado, current)

            if not posicoes_iguais(current, new_pos):
                if eh_posicao_animal(prado, new_pos):   # Comer
                    reset_fome(animal)
                    eliminar_animal(prado, new_pos)
                    mover_animal(prado, current, new_pos)
                    already_moved += (posicao_para_str(new_pos),)
                else:                                   # mover sem comer
                    mover_animal(prado, current, new_pos)

                if eh_animal_fertil(animal):            # reproduzir
                    filho = reproduz_animal(obter_animal(prado, new_pos))
                    inserir_animal(prado, filho, current)

            if eh_animal_faminto(obter_animal(prado, new_pos)):
                eliminar_animal(prado, new_pos)

    return prado

# Esta funcao é apenas publica para utulizar com a GUI
def parse_config(filename):
    with open(filename, 'r') as file:
        dim = cria_posicao(*eval(file.readline()))
        obs = tuple(cria_posicao(*p) for p in eval(file.readline()))

        an, pos = (), ()
        for line in file.readlines():
            line = eval(line)
            an += (cria_animal(*line[:3]),)
            pos += (cria_posicao(*line[3]),)

    return cria_prado(dim, obs, an, pos)

def simula_ecossistema(filename, num_gens, verbose):
    def show_status(prado, gen):
        print('Predadores: {} vs Presas: {} (Gen. {})'.
              format(obter_numero_predadores(prado), obter_numero_presas(prado), gen))
        print(prado_para_str(prado))

    def numero_animais(m):
        return obter_numero_predadores(m), obter_numero_presas(m)

    prado = parse_config(filename)
    show_status(prado, 0)

    for gen in range(1, num_gens+1):
        old_prado = cria_copia_prado(prado)
        prado = geracao(prado)

        if verbose and numero_animais(prado) != numero_animais(old_prado):
            show_status(prado, gen)

    if not verbose:
        show_status(prado, gen)

    return numero_animais(prado)
